1 module feature;
2 import commons;
3 public import std.functional:toDelegate;
4 
5 ///URLs supports $VERSION
6 struct DownloadURL
7 {
8     string windows;
9     string linux;
10     string osx;
11 
12     static DownloadURL any(string url)
13     {
14         return DownloadURL(url, url, url);
15     }
16 
17     string get(TargetVersion ver) const
18     {
19         import std.string:replace;
20         string ret;
21         version(Windows) ret = windows;
22         else version(linux) ret = linux;
23         else version(OSX) ret = osx;
24         return ret.replace("$VERSION", ver.toString);
25     }
26     string getDownloadFileName(TargetVersion ver) const
27     {
28         return get(ver).baseName;
29     }
30 }
31 
32 struct Download
33 {
34     DownloadURL url;
35     ///Supports $CWD, $TEMP, $VERSION and $NAME
36     string outputPath = "$TEMP$NAME";
37     ///Negative version ignored.
38     TargetVersion ver;
39     void function(string outputPath) onDownloadFinish;
40 
41     bool download(ref Terminal t, ref RealTimeConsoleInput input, TargetVersion ver)
42     {
43         this.ver = ver;
44         commons.downloadWithProgressBar(t, url.get(ver), getOutputPath(ver));
45         return true;
46     }
47     string getOutputPath() const
48     {
49         return getOutputPath(ver);
50     }
51 
52     string getOutputPath(TargetVersion ver) const
53     {
54         import std.conv:to;
55         import std.string;
56         string ret = replace(outputPath, "$CWD", std.file.getcwd);
57         ret = replace(ret, "$TEMP", std.file.tempDir);
58         ret = replace(ret, "$NAME", url.getDownloadFileName(ver));
59         ret = replace(ret, "$VERSION", ver.toString);
60         return ret;
61     }
62 }
63 
64 struct Installation
65 {
66     Download[] downloadsRequired;
67     bool delegate(
68         ref Terminal t, 
69         ref RealTimeConsoleInput input, 
70         TargetVersion ver, 
71         Download[] content
72     ) installer;
73 
74     /** 
75      * Follows the same order from `downloadsRequired`
76      * May receive null if no extraction is desired for the download.
77      * Accepts $CWD and $VERSION
78      */
79     string[] extractionPathList;
80 
81     string getExtractionPath(size_t index, TargetVersion ver)
82     {
83         import std.string;
84         return extractionPathList[index].replace("$CWD", std.file.getcwd).replace("$VERSION", ver.toString).buildNormalizedPath;
85     }
86 
87     bool install(ref Terminal t, ref RealTimeConsoleInput input, TargetVersion ver)
88     {
89         foreach(i, ref d; downloadsRequired)
90         {
91             if(!std.file.exists(d.getOutputPath(ver)))
92             {
93                 t.writeln("Downloading ", d.url.get(ver), " --> ", d.getOutputPath(ver));
94                 t.flush;
95                 if(!d.download(t, input, ver)) return false;
96             }
97             if(i < extractionPathList.length && extractionPathList[i].length)
98             {
99                 import std.file;
100                 string extractionPath = getExtractionPath(i, ver);
101                 
102                 if(!extractToFolder(d.getOutputPath(ver), extractionPath, t, input))
103                     return false;
104             }
105         }
106         if(installer)
107             return installer(t, input, ver, downloadsRequired);
108         return true;
109     }
110 
111 }
112 
113 struct Task(alias Fn)
114 {
115     import std.traits;
116     import std.meta;
117 
118     Feature*[] dependencies;
119     private static auto fn = &Fn;
120     static assert(is(Parameters!Fn[0] == Feature*[]), "The first argument of a Task function must be Feature*[]");
121 
122     auto execute(Parameters!Fn[1..$] args)
123     {
124         if(dependencies.length == 0)
125             throw new Error("Your task has no dependency. Maybe you forgot to include it in the mixin StartFeatures! list?");
126         foreach(dep; dependencies)
127         {
128             if(!dep.getFeature(args[0], args[1]))
129                 throw new Exception("Could not get the feature named '"~dep.name~"' for executing the task.");
130         }
131         return fn(dependencies, args);
132     }   
133 }
134 
135 public import std.system;
136 struct Feature
137 {
138     string name;
139     string description;
140     /** 
141      * Checks the existence in $PATH
142      * Checks the existence in gameBuild
143      */
144     ExistenceChecker existenceChecker;
145     /** 
146      * Gets an optional Download[] array, and an installer function
147      * which contains the downloaded files information
148      */
149     Installation installer;
150     /** 
151      * A function that is executed exactly once after the installation
152      * was succeeded.
153      */
154     void function(ref Terminal t, string where) startUsingFeature;
155     /** 
156      * Range of supported versions. May support in the feature also
157      * version whitelisting. 
158      */
159     VersionRange supportedVersion;
160     /**
161      * The version that was actually chosen
162      */
163     TargetVersion currentVersion;
164     /**
165     * When empty it means it is required on every OS.
166     * This was made because if it is not required in any OS, simply don't
167     * put in the dependencies
168     */
169     OS[] requiredOn;
170 
171     /** 
172      * Dependencies must be initialized in a 2-way start.
173      * First, every dependency is started with its own information
174      * After that, all the dependencies are started.
175      */
176     Feature*[] dependencies;
177 
178     bool isRequired()
179     {
180         if(requiredOn.length == 0)
181             return true;
182         foreach(req; requiredOn) if(req == os) return true;
183         return false;
184     }
185     
186 
187     Feature*[] getAllDependencies()
188     {
189         bool[Feature*] visited;
190         Feature*[] ret;
191         foreach(dep; dependencies)
192         {
193             if(!(dep in visited))
194             {
195                 visited[dep] = true;
196                 if(dep.isRequired)
197                 {
198                     ret~= dep;
199                     ret~= dep.getAllDependencies;
200                 }
201             }
202         }
203         return ret.unique;
204     }
205 
206     private bool startedUsing = false;
207 
208     bool getFeature(ref Terminal t, ref RealTimeConsoleInput input, TargetVersion v = TargetVersion.init)
209     {
210         if(v == TargetVersion.init)
211         {
212             if(currentVersion != TargetVersion.init)
213                 v = currentVersion;
214             else
215             {
216                 v = supportedVersion.max;
217                 currentVersion = v;
218             }
219         }
220         if(!supportedVersion.isInRange(v))
221         {
222             t.writelnError("Unsupported version '",v.toString,"' for feature ", name, 
223                 ".\n\t Supported versions are ", supportedVersion.toString);
224             return false;
225         }
226         foreach(Feature* dep; getAllDependencies)
227         {
228             if(*dep != Feature.init && !dep.getFeature(t, input, dep.supportedVersion.max))
229             {
230                 t.writelnError("Could not get feature '",name,"': Requires: ", dep.name);
231                 return false;
232             }
233         }
234         ExistenceStatus status = existenceChecker.existStatus(t, v);
235         if(status.place == ExistenceStatus.Place.notFound)
236         {
237             import std.conv:to;
238             t.writeln("Installation: ", name, " v", v.toString, "\n\t", description);
239             t.flush;
240             if(!installer.install(t, input, v))
241             {
242                 t.writelnError("Could not install feature ", name);
243                 return false;
244             }
245             status = existenceChecker.existStatus(t, v);
246         }
247         if(status.place == ExistenceStatus.Place.notFound)
248             throw new Error(`Could not find `~name~` v`~v.toString~"\n\t"~description~" even after installation");
249         if(!startedUsing)
250         {
251             startedUsing = true;
252             if(startUsingFeature !is null)
253             {
254                 string where = status.where;
255                 if(status.place == ExistenceStatus.Place.inConfig)
256                     where = configs[where].str;
257                 startUsingFeature(t, where);
258             }
259         }
260         return true;
261     }
262 }
263 mixin template StartFeatures(string[] features)
264 {
265     static this()
266     {
267         static foreach(f; features)
268         {
269             mixin("import ",f," = features.",f,";");
270             mixin(f,".initialize();");
271         }
272         static foreach(f; features)
273         {
274             mixin(f,".start();");
275         }
276     }
277 }
278 
279 struct ExistenceStatus
280 {
281     static enum Place
282     {
283         notFound,
284         inConfig,
285         inPath,
286         custom
287     }
288     Place place;
289     string where;
290 }
291 
292 struct ExistenceChecker
293 {
294     ///All the inputs related to this feature.
295     string[] gameBuildInput;
296     ///All the aliases this feature is expected in path.
297     string[] expectedInPathAs;
298     ///Optional. 
299     bool delegate(ref Terminal t, TargetVersion v, out ExistenceStatus where) checkExistenceFn;
300 
301 
302     ExistenceStatus existStatus(ref Terminal t, TargetVersion v)
303     {
304         ExistenceStatus status;
305         int validCount = 0;
306         foreach(i; gameBuildInput)
307             if(i in configs && std.file.exists(configs[i].str)) validCount++;
308         if(validCount && validCount == gameBuildInput.length)
309         {
310             status.place = ExistenceStatus.Place.inConfig;
311             status.where = gameBuildInput[0];
312             return status;
313         }
314         foreach(anAlias; expectedInPathAs)
315         {
316             string program = findProgramPath(anAlias);
317             if(program.length)
318             {
319                 status.where = program;
320                 status. place = ExistenceStatus.Place.inPath;
321                 return status;
322             }
323         }
324         if(checkExistenceFn)
325             checkExistenceFn(t, v, status);
326         return status;
327     }
328 
329 }
330 struct TargetVersion
331 {
332     static struct Modifier
333     {
334         ///May receive a different modifier name.
335         string name;
336         int ver = -1;
337     }
338     int major = -1;
339     int minor = -1;
340     int patch = -1;
341     Modifier modifier;
342 
343     string toString()
344     {
345         import std.conv:to;
346         if(major == -1) return null;
347         string ret = major.to!string;
348         if(minor != -1) ret~= "." ~ minor.to!string;
349         if(patch != -1) ret~= "." ~ patch.to!string;
350         if(modifier.name.length) ret~= modifier.name;
351         if(modifier.ver != -1) ret~= modifier.ver.to!string;
352         return ret;
353     }
354 
355     static TargetVersion fromGameBuild(string entry)
356     {
357         if(!(entry in configs))
358             return TargetVersion.init;
359         return TargetVersion.parse(configs[entry].str);
360     }
361 
362     static TargetVersion parse(string ver)
363     {
364         import std.conv:to;
365         string[] vers = ver.split(".");
366         TargetVersion ret;
367 
368         if(vers.length > 3) throw new Error("Unsupported format "~ver);
369         if(vers.length > 0) ret.major = vers[0].to!int;
370         if(vers.length > 1) ret.minor = vers[1].to!int;
371         if(vers.length > 2)
372         {
373             import std.algorithm;
374             ptrdiff_t ind = countUntil!((a) => a < '0' || a > '9')(vers[2]);
375             if(ind == -1) ret.patch = vers[2].to!int; 
376             else
377             {
378                 ret.patch = vers[2][0..ind].to!int;
379                 ptrdiff_t ver2 = countUntil!((a) => a >= '0' && a <= '9')(vers[2][ind..$]);
380                 if(ver2 == -1) ret.modifier.name = vers[2][ind..$];
381                 else
382                 {
383                     ret.modifier.name = vers[2][ind..ind+ver2];
384                     ret.modifier.ver = vers[2][ind+ver2..$].to!int;
385                 }
386 
387             }
388         }
389         return ret;
390     }
391 
392     unittest
393     {
394         TargetVersion v = TargetVersion.parse("1.36.0-beta1");
395         assert(v.toString == "1.36.0-beta1");
396         assert(v.major == 1);
397         assert(v.minor == 36);
398         assert(v.patch == 0);
399         assert(v.modifier.name == "-beta");
400         assert(v.modifier.ver == 1);
401     }
402 }
403 
404 struct VersionRange
405 {
406     TargetVersion min, max;
407     static VersionRange parse(string min, string max = null)
408     {
409         if(max == null) max = min;
410         return VersionRange(TargetVersion.parse(min), TargetVersion.parse(max));
411     }
412 
413     string toString()
414     {
415         return min.toString ~ " ~ " ~ max.toString;
416     }
417     /** 
418      * Compares both major and minor to min and max versions.
419      * Currently, patch, modifier and modifier version are ignored.
420      * Params:
421      *   v = A Target version
422      * Returns: 
423      */
424     bool isInRange(TargetVersion v)
425     {
426         if(this == VersionRange.init)
427             return true;
428         return v.major >= min.major  && v.major <= max.major &&
429         v.minor >= min.minor && v.minor <= max.minor;
430     }
431 }